/*
 * Copyright 2001-2018 Haiku, Inc. All rights reserved.
 * Distributed under the terms of the MIT license.
 *
 * Authors:
 *		Stephan Aßmus, superstippi@gmx.de
 *		Stefano Ceccherini, stefano.ceccherini@gmail.com
 *		Adrien Destugues, pulkomandy@pulkomandy.tk
 *		Marc Flerackers, mflerackers@androme.be
 *		Rene Gollent, anevilyak@gmail.com
 *		John Scipione, jscipione@gmail.com
 */


#include <Menu.h>

#include <algorithm>
#include <new>
#include <set>

#include <ctype.h>
#include <string.h>

#include <Application.h>
#include <Bitmap.h>
#include <ControlLook.h>
#include <Debug.h>
#include <File.h>
#include <FindDirectory.h>
#include <Layout.h>
#include <LayoutUtils.h>
#include <MenuBar.h>
#include <MenuItem.h>
#include <Messenger.h>
#include <Path.h>
#include <PropertyInfo.h>
#include <Screen.h>
#include <ScrollBar.h>
#include <SystemCatalog.h>
#include <UnicodeChar.h>
#include <Window.h>

#include <AppServerLink.h>
#include <AutoDeleter.h>
#include <binary_compatibility/Interface.h>
#include <BMCPrivate.h>
#include <MenuPrivate.h>
#include <MenuWindow.h>
#include <ServerProtocol.h>

#include "utf8_functions.h"


#define USE_CACHED_MENUWINDOW 1

using BPrivate::gSystemCatalog;

#undef B_TRANSLATION_CONTEXT
#define B_TRANSLATION_CONTEXT "Menu"

#undef B_TRANSLATE
#define B_TRANSLATE(str) \
	gSystemCatalog.GetString(B_TRANSLATE_MARK(str), "Menu")


using std::nothrow;
using BPrivate::BMenuWindow;

namespace BPrivate {

class TriggerList {
public:
	TriggerList() {}
	~TriggerList() {}

	bool HasTrigger(uint32 c)
		{ return fList.find(BUnicodeChar::ToLower(c)) != fList.end(); }

	bool AddTrigger(uint32 c)
	{
		fList.insert(BUnicodeChar::ToLower(c));
		return true;
	}

private:
	std::set<uint32> fList;
};


class ExtraMenuData {
public:
	menu_tracking_hook	trackingHook;
	void*				trackingState;

	// Used to track when the menu would be drawn offscreen and instead gets
	// shifted back on the screen towards the left. This information
	// allows us to draw submenus in the same direction as their parents.
	bool				frameShiftedLeft;

	ExtraMenuData()
	{
		trackingHook = NULL;
		trackingState = NULL;
		frameShiftedLeft = false;
	}
};


typedef int (*compare_func)(const BMenuItem*, const BMenuItem*);

struct MenuItemComparator
{
	MenuItemComparator(compare_func compareFunc)
		:
		fCompareFunc(compareFunc)
	{
	}

	bool operator () (const BMenuItem* item1, const BMenuItem* item2) {
		return fCompareFunc(item1, item2) < 0;
	}

private:
	compare_func fCompareFunc;
};


}	// namespace BPrivate


menu_info BMenu::sMenuInfo;

uint32 BMenu::sShiftKey;
uint32 BMenu::sControlKey;
uint32 BMenu::sOptionKey;
uint32 BMenu::sCommandKey;
uint32 BMenu::sMenuKey;

static property_info sPropList[] = {
	{ "Enabled", { B_GET_PROPERTY, 0 },
		{ B_DIRECT_SPECIFIER, 0 }, "Returns true if menu or menu item is "
		"enabled; false otherwise.",
		0, { B_BOOL_TYPE }
	},

	{ "Enabled", { B_SET_PROPERTY, 0 },
		{ B_DIRECT_SPECIFIER, 0 }, "Enables or disables menu or menu item.",
		0, { B_BOOL_TYPE }
	},

	{ "Label", { B_GET_PROPERTY, 0 },
		{ B_DIRECT_SPECIFIER, 0 }, "Returns the string label of the menu or "
		"menu item.",
		0, { B_STRING_TYPE }
	},

	{ "Label", { B_SET_PROPERTY, 0 },
		{ B_DIRECT_SPECIFIER, 0 }, "Sets the string label of the menu or menu "
		"item.",
		0, { B_STRING_TYPE }
	},

	{ "Mark", { B_GET_PROPERTY, 0 },
		{ B_DIRECT_SPECIFIER, 0 }, "Returns true if the menu item or the "
		"menu's superitem is marked; false otherwise.",
		0, { B_BOOL_TYPE }
	},

	{ "Mark", { B_SET_PROPERTY, 0 },
		{ B_DIRECT_SPECIFIER, 0 }, "Marks or unmarks the menu item or the "
		"menu's superitem.",
		0, { B_BOOL_TYPE }
	},

	{ "Menu", { B_CREATE_PROPERTY, 0 },
		{ B_NAME_SPECIFIER, B_INDEX_SPECIFIER, B_REVERSE_INDEX_SPECIFIER, 0 },
		"Adds a new menu item at the specified index with the text label "
		"found in \"data\" and the int32 command found in \"what\" (used as "
		"the what field in the BMessage sent by the item)." , 0, {},
		{ 	{{{"data", B_STRING_TYPE}}}
		}
	},

	{ "Menu", { B_DELETE_PROPERTY, 0 },
		{ B_NAME_SPECIFIER, B_INDEX_SPECIFIER, B_REVERSE_INDEX_SPECIFIER, 0 },
		"Removes the selected menu or menus.", 0, {}
	},

	{ "Menu", { },
		{ B_NAME_SPECIFIER, B_INDEX_SPECIFIER, B_REVERSE_INDEX_SPECIFIER, 0 },
		"Directs scripting message to the specified menu, first popping the "
		"current specifier off the stack.", 0, {}
	},

	{ "MenuItem", { B_COUNT_PROPERTIES, 0 },
		{ B_DIRECT_SPECIFIER, 0 }, "Counts the number of menu items in the "
		"specified menu.",
		0, { B_INT32_TYPE }
	},

	{ "MenuItem", { B_CREATE_PROPERTY, 0 },
		{ B_NAME_SPECIFIER, B_INDEX_SPECIFIER, B_REVERSE_INDEX_SPECIFIER, 0 },
		"Adds a new menu item at the specified index with the text label "
		"found in \"data\" and the int32 command found in \"what\" (used as "
		"the what field in the BMessage sent by the item).", 0, {},
		{	{ {{"data", B_STRING_TYPE },
			{"be:invoke_message", B_MESSAGE_TYPE},
			{"what", B_INT32_TYPE},
			{"be:target", B_MESSENGER_TYPE}} }
		}
	},

	{ "MenuItem", { B_DELETE_PROPERTY, 0 },
		{ B_NAME_SPECIFIER, B_INDEX_SPECIFIER, B_REVERSE_INDEX_SPECIFIER, 0 },
		"Removes the specified menu item from its parent menu."
	},

	{ "MenuItem", { B_EXECUTE_PROPERTY, 0 },
		{ B_NAME_SPECIFIER, B_INDEX_SPECIFIER, B_REVERSE_INDEX_SPECIFIER, 0 },
		"Invokes the specified menu item."
	},

	{ "MenuItem", { },
		{ B_NAME_SPECIFIER, B_INDEX_SPECIFIER, B_REVERSE_INDEX_SPECIFIER, 0 },
		"Directs scripting message to the specified menu, first popping the "
		"current specifier off the stack."
	},

	{ 0 }
};


// note: this is redefined to localized one in BMenu::_InitData
const char* BPrivate::kEmptyMenuLabel = "<empty>";


struct BMenu::LayoutData {
	BSize	preferred;
	uint32	lastResizingMode;
};


// #pragma mark - BMenu


BMenu::BMenu(const char* name, menu_layout layout)
	:
	BView(BRect(0, 0, 0, 0), name, 0, B_WILL_DRAW),
	fChosenItem(NULL),
	fSelected(NULL),
	fCachedMenuWindow(NULL),
	fSuper(NULL),
	fSuperitem(NULL),
	fAscent(-1.0f),
	fDescent(-1.0f),
	fFontHeight(-1.0f),
	fState(MENU_STATE_CLOSED),
	fLayout(layout),
	fExtraRect(NULL),
	fMaxContentWidth(0.0f),
	fInitMatrixSize(NULL),
	fExtraMenuData(NULL),
	fTrigger(0),
	fResizeToFit(true),
	fUseCachedMenuLayout(false),
	fEnabled(true),
	fDynamicName(false),
	fRadioMode(false),
	fTrackNewBounds(false),
	fStickyMode(false),
	fIgnoreHidden(true),
	fTriggerEnabled(true),
	fHasSubmenus(false),
	fAttachAborted(false)
{
	_InitData(NULL);
}


BMenu::BMenu(const char* name, float width, float height)
	:
	BView(BRect(0.0f, 0.0f, 0.0f, 0.0f), name, 0, B_WILL_DRAW),
	fChosenItem(NULL),
	fSelected(NULL),
	fCachedMenuWindow(NULL),
	fSuper(NULL),
	fSuperitem(NULL),
	fAscent(-1.0f),
	fDescent(-1.0f),
	fFontHeight(-1.0f),
	fState(0),
	fLayout(B_ITEMS_IN_MATRIX),
	fExtraRect(NULL),
	fMaxContentWidth(0.0f),
	fInitMatrixSize(NULL),
	fExtraMenuData(NULL),
	fTrigger(0),
	fResizeToFit(true),
	fUseCachedMenuLayout(false),
	fEnabled(true),
	fDynamicName(false),
	fRadioMode(false),
	fTrackNewBounds(false),
	fStickyMode(false),
	fIgnoreHidden(true),
	fTriggerEnabled(true),
	fHasSubmenus(false),
	fAttachAborted(false)
{
	_InitData(NULL);
}


BMenu::BMenu(BMessage* archive)
	:
	BView(archive),
	fChosenItem(NULL),
	fSelected(NULL),
	fCachedMenuWindow(NULL),
	fSuper(NULL),
	fSuperitem(NULL),
	fAscent(-1.0f),
	fDescent(-1.0f),
	fFontHeight(-1.0f),
	fState(MENU_STATE_CLOSED),
	fLayout(B_ITEMS_IN_ROW),
	fExtraRect(NULL),
	fMaxContentWidth(0.0f),
	fInitMatrixSize(NULL),
	fExtraMenuData(NULL),
	fTrigger(0),
	fResizeToFit(true),
	fUseCachedMenuLayout(false),
	fEnabled(true),
	fDynamicName(false),
	fRadioMode(false),
	fTrackNewBounds(false),
	fStickyMode(false),
	fIgnoreHidden(true),
	fTriggerEnabled(true),
	fHasSubmenus(false),
	fAttachAborted(false)
{
	_InitData(archive);
}


BMenu::~BMenu()
{
	_DeleteMenuWindow();

	RemoveItems(0, CountItems(), true);

	delete fInitMatrixSize;
	delete fExtraMenuData;
	delete fLayoutData;
}


BArchivable*
BMenu::Instantiate(BMessage* archive)
{
	if (validate_instantiation(archive, "BMenu"))
		return new (nothrow) BMenu(archive);

	return NULL;
}


status_t
BMenu::Archive(BMessage* data, bool deep) const
{
	status_t err = BView::Archive(data, deep);

	if (err == B_OK && Layout() != B_ITEMS_IN_ROW)
		err = data->AddInt32("_layout", Layout());
	if (err == B_OK)
		err = data->AddBool("_rsize_to_fit", fResizeToFit);
	if (err == B_OK)
		err = data->AddBool("_disable", !IsEnabled());
	if (err ==  B_OK)
		err = data->AddBool("_radio", IsRadioMode());
	if (err == B_OK)
		err = data->AddBool("_trig_disabled", AreTriggersEnabled());
	if (err == B_OK)
		err = data->AddBool("_dyn_label", fDynamicName);
	if (err == B_OK)
		err = data->AddFloat("_maxwidth", fMaxContentWidth);
	if (err == B_OK && deep) {
		BMenuItem* item = NULL;
		int32 index = 0;
		while ((item = ItemAt(index++)) != NULL) {
			BMessage itemData;
			item->Archive(&itemData, deep);
			err = data->AddMessage("_items", &itemData);
			if (err != B_OK)
				break;
			if (fLayout == B_ITEMS_IN_MATRIX) {
				err = data->AddRect("_i_frames", item->fBounds);
			}
		}
	}

	return err;
}


void
BMenu::AttachedToWindow()
{
	BView::AttachedToWindow();

	_GetShiftKey(sShiftKey);
	_GetControlKey(sControlKey);
	_GetCommandKey(sCommandKey);
	_GetOptionKey(sOptionKey);
	_GetMenuKey(sMenuKey);

	// The menu should be added to the menu hierarchy and made visible if:
	// * the mouse is over the menu,
	// * the user has requested the menu via the keyboard.
	// So if we don't pass keydown in here, keyboard navigation breaks since
	// fAttachAborted will return false if the mouse isn't over the menu
	bool keyDown = Supermenu() != NULL
		? Supermenu()->fState == MENU_STATE_KEY_TO_SUBMENU : false;
	fAttachAborted = _AddDynamicItems(keyDown);

	if (!fAttachAborted) {
		_CacheFontInfo();
		_LayoutItems(0);
		_UpdateWindowViewSize(false);
	}
}


void
BMenu::DetachedFromWindow()
{
	BView::DetachedFromWindow();
}


void
BMenu::AllAttached()
{
	BView::AllAttached();
}


void
BMenu::AllDetached()
{
	BView::AllDetached();
}


void
BMenu::Draw(BRect updateRect)
{
	if (_RelayoutIfNeeded()) {
		Invalidate();
		return;
	}

	DrawBackground(updateRect);
	DrawItems(updateRect);
}


void
BMenu::MessageReceived(BMessage* message)
{
	if (message->HasSpecifiers())
		return _ScriptReceived(message);

	switch (message->what) {
		case B_MOUSE_WHEEL_CHANGED:
		{
			float deltaY = 0;
			message->FindFloat("be:wheel_delta_y", &deltaY);
			if (deltaY == 0)
				return;

			BMenuWindow* window = dynamic_cast<BMenuWindow*>(Window());
			if (window == NULL)
				return;

			float largeStep;
			float smallStep;
			window->GetSteps(&smallStep, &largeStep);

			// pressing the shift key scrolls faster
			if ((modifiers() & B_SHIFT_KEY) != 0)
				deltaY *= largeStep;
			else
				deltaY *= smallStep;

			window->TryScrollBy(deltaY);
			break;
		}

		default:
			BView::MessageReceived(message);
			break;
	}
}


void
BMenu::KeyDown(const char* bytes, int32 numBytes)
{
	// TODO: Test how it works on BeOS R5 and implement this correctly
	switch (bytes[0]) {
		case B_UP_ARROW:
		case B_DOWN_ARROW:
		{
			BMenuBar* bar = dynamic_cast<BMenuBar*>(Supermenu());
			if (bar != NULL && fState == MENU_STATE_CLOSED) {
				// tell MenuBar's _Track:
				bar->fState = MENU_STATE_KEY_TO_SUBMENU;
			}
			if (fLayout == B_ITEMS_IN_COLUMN)
				_SelectNextItem(fSelected, bytes[0] == B_DOWN_ARROW);
			break;
		}

		case B_LEFT_ARROW:
			if (fLayout == B_ITEMS_IN_ROW)
				_SelectNextItem(fSelected, false);
			else {
				// this case has to be handled a bit specially.
				BMenuItem* item = Superitem();
				if (item) {
					if (dynamic_cast<BMenuBar*>(Supermenu())) {
						// If we're at the top menu below the menu bar, pass
						// the keypress to the menu bar so we can move to
						// another top level menu.
						BMessenger messenger(Supermenu());
						messenger.SendMessage(Window()->CurrentMessage());
					} else {
						// tell _Track
						fState = MENU_STATE_KEY_LEAVE_SUBMENU;
					}
				}
			}
			break;

		case B_RIGHT_ARROW:
			if (fLayout == B_ITEMS_IN_ROW)
				_SelectNextItem(fSelected, true);
			else {
				if (fSelected != NULL && fSelected->Submenu() != NULL) {
					fSelected->Submenu()->_SetStickyMode(true);
						// fix me: this shouldn't be needed but dynamic menus
						// aren't getting it set correctly when keyboard
						// navigating, which aborts the attach
					fState = MENU_STATE_KEY_TO_SUBMENU;
					_SelectItem(fSelected, true, true, true);
				} else if (dynamic_cast<BMenuBar*>(Supermenu())) {
					// if we have no submenu and we're an
					// item in the top menu below the menubar,
					// pass the keypress to the menubar
					// so you can use the keypress to switch menus.
					BMessenger messenger(Supermenu());
					messenger.SendMessage(Window()->CurrentMessage());
				}
			}
			break;

		case B_PAGE_UP:
		case B_PAGE_DOWN:
		{
			BMenuWindow* window = dynamic_cast<BMenuWindow*>(Window());
			if (window == NULL || !window->HasScrollers())
				break;

			int32 deltaY = bytes[0] == B_PAGE_UP ? -1 : 1;

			float largeStep;
			window->GetSteps(NULL, &largeStep);
			window->TryScrollBy(deltaY * largeStep);
			break;
		}

		case B_ENTER:
		case B_SPACE:
			if (fSelected != NULL) {
				fChosenItem = fSelected;
					// preserve for exit handling
				_QuitTracking(false);
			}
			break;

		case B_ESCAPE:
			_SelectItem(NULL);
			if (fState == MENU_STATE_CLOSED
				&& dynamic_cast<BMenuBar*>(Supermenu())) {
				// Keyboard may show menu without tracking it
				BMessenger messenger(Supermenu());
				messenger.SendMessage(Window()->CurrentMessage());
			} else
				_QuitTracking(false);
			break;

		default:
		{
			if (AreTriggersEnabled()) {
				uint32 trigger = BUnicodeChar::FromUTF8(&bytes);

				for (uint32 i = CountItems(); i-- > 0;) {
					BMenuItem* item = ItemAt(i);
					if (item->fTriggerIndex < 0 || item->fTrigger != trigger)
						continue;

					_InvokeItem(item);
					_QuitTracking(false);
					break;
				}
			}
			break;
		}
	}
}


BSize
BMenu::MinSize()
{
	_ValidatePreferredSize();

	BSize size = (GetLayout() != NULL ? GetLayout()->MinSize()
		: fLayoutData->preferred);

	return BLayoutUtils::ComposeSize(ExplicitMinSize(), size);
}


BSize
BMenu::MaxSize()
{
	_ValidatePreferredSize();

	BSize size = (GetLayout() != NULL ? GetLayout()->MaxSize()
		: fLayoutData->preferred);

	return BLayoutUtils::ComposeSize(ExplicitMaxSize(), size);
}


BSize
BMenu::PreferredSize()
{
	_ValidatePreferredSize();

	BSize size = (GetLayout() != NULL ? GetLayout()->PreferredSize()
		: fLayoutData->preferred);

	return BLayoutUtils::ComposeSize(ExplicitPreferredSize(), size);
}


void
BMenu::GetPreferredSize(float* _width, float* _height)
{
	_ValidatePreferredSize();

	if (_width)
		*_width = fLayoutData->preferred.width;

	if (_height)
		*_height = fLayoutData->preferred.height;
}


void
BMenu::ResizeToPreferred()
{
	BView::ResizeToPreferred();
}


void
BMenu::DoLayout()
{
	// If the user set a layout, we let the base class version call its
	// hook.
	if (GetLayout() != NULL) {
		BView::DoLayout();
		return;
	}

	if (_RelayoutIfNeeded())
		Invalidate();
}


void
BMenu::FrameMoved(BPoint where)
{
	BView::FrameMoved(where);
}


void
BMenu::FrameResized(float width, float height)
{
	BView::FrameResized(width, height);
}


void
BMenu::InvalidateLayout()
{
	fUseCachedMenuLayout = false;
	// This method exits for backwards compatibility reasons, it is used to
	// invalidate the menu layout, but we also use call
	// BView::InvalidateLayout() for good measure. Don't delete this method!
	BView::InvalidateLayout(false);
}


void
BMenu::MakeFocus(bool focused)
{
	BView::MakeFocus(focused);
}


bool
BMenu::AddItem(BMenuItem* item)
{
	return AddItem(item, CountItems());
}


bool
BMenu::AddItem(BMenuItem* item, int32 index)
{
	if (fLayout == B_ITEMS_IN_MATRIX) {
		debugger("BMenu::AddItem(BMenuItem*, int32) this method can only "
			"be called if the menu layout is not B_ITEMS_IN_MATRIX");
	}

	if (item == NULL)
		return false;

	const bool locked = LockLooper();

	if (!_AddItem(item, index)) {
		if (locked)
			UnlockLooper();
		return false;
	}

	InvalidateLayout();
	if (locked) {
		if (!Window()->IsHidden()) {
			_LayoutItems(index);
			_UpdateWindowViewSize(false);
			Invalidate();
		}
		UnlockLooper();
	}

	return true;
}


bool
BMenu::AddItem(BMenuItem* item, BRect frame)
{
	if (fLayout != B_ITEMS_IN_MATRIX) {
		debugger("BMenu::AddItem(BMenuItem*, BRect) this method can only "
			"be called if the menu layout is B_ITEMS_IN_MATRIX");
	}

	if (item == NULL)
		return false;

	const bool locked = LockLooper();

	item->fBounds = frame;

	int32 index = CountItems();
	if (!_AddItem(item, index)) {
		if (locked)
			UnlockLooper();
		return false;
	}

	if (locked) {
		if (!Window()->IsHidden()) {
			_LayoutItems(index);
			Invalidate();
		}
		UnlockLooper();
	}

	return true;
}


bool
BMenu::AddItem(BMenu* submenu)
{
	BMenuItem* item = new (nothrow) BMenuItem(submenu);
	if (item == NULL)
		return false;

	if (!AddItem(item, CountItems())) {
		item->fSubmenu = NULL;
		delete item;
		return false;
	}

	return true;
}


bool
BMenu::AddItem(BMenu* submenu, int32 index)
{
	if (fLayout == B_ITEMS_IN_MATRIX) {
		debugger("BMenu::AddItem(BMenuItem*, int32) this method can only "
			"be called if the menu layout is not B_ITEMS_IN_MATRIX");
	}

	BMenuItem* item = new (nothrow) BMenuItem(submenu);
	if (item == NULL)
		return false;

	if (!AddItem(item, index)) {
		item->fSubmenu = NULL;
		delete item;
		return false;
	}

	return true;
}


bool
BMenu::AddItem(BMenu* submenu, BRect frame)
{
	if (fLayout != B_ITEMS_IN_MATRIX) {
		debugger("BMenu::AddItem(BMenu*, BRect) this method can only "
			"be called if the menu layout is B_ITEMS_IN_MATRIX");
	}

	BMenuItem* item = new (nothrow) BMenuItem(submenu);
	if (item == NULL)
		return false;

	if (!AddItem(item, frame)) {
		item->fSubmenu = NULL;
		delete item;
		return false;
	}

	return true;
}


bool
BMenu::AddList(BList* list, int32 index)
{
	// TODO: test this function, it's not documented in the bebook.
	if (list == NULL)
		return false;

	bool locked = LockLooper();

	int32 numItems = list->CountItems();
	for (int32 i = 0; i < numItems; i++) {
		BMenuItem* item = static_cast<BMenuItem*>(list->ItemAt(i));
		if (item != NULL) {
			if (!_AddItem(item, index + i))
				break;
		}
	}

	InvalidateLayout();
	if (locked && Window() != NULL && !Window()->IsHidden()) {
		// Make sure we update the layout if needed.
		_LayoutItems(index);
		_UpdateWindowViewSize(false);
		Invalidate();
	}

	if (locked)
		UnlockLooper();

	return true;
}


bool
BMenu::AddSeparatorItem()
{
	BMenuItem* item = new (nothrow) BSeparatorItem();
	if (!item || !AddItem(item, CountItems())) {
		delete item;
		return false;
	}

	return true;
}


bool
BMenu::RemoveItem(BMenuItem* item)
{
	return _RemoveItems(0, 0, item, false);
}


BMenuItem*
BMenu::RemoveItem(int32 index)
{
	BMenuItem* item = ItemAt(index);
	if (item != NULL)
		_RemoveItems(index, 1, NULL, false);
	return item;
}


bool
BMenu::RemoveItems(int32 index, int32 count, bool deleteItems)
{
	return _RemoveItems(index, count, NULL, deleteItems);
}


bool
BMenu::RemoveItem(BMenu* submenu)
{
	for (int32 i = 0; i < fItems.CountItems(); i++) {
		if (static_cast<BMenuItem*>(fItems.ItemAtFast(i))->Submenu()
				== submenu) {
			return _RemoveItems(i, 1, NULL, false);
		}
	}

	return false;
}


int32
BMenu::CountItems() const
{
	return fItems.CountItems();
}


BMenuItem*
BMenu::ItemAt(int32 index) const
{
	return static_cast<BMenuItem*>(fItems.ItemAt(index));
}


BMenu*
BMenu::SubmenuAt(int32 index) const
{
	BMenuItem* item = static_cast<BMenuItem*>(fItems.ItemAt(index));
	return item != NULL ? item->Submenu() : NULL;
}


int32
BMenu::IndexOf(BMenuItem* item) const
{
	return fItems.IndexOf(item);
}


int32
BMenu::IndexOf(BMenu* submenu) const
{
	for (int32 i = 0; i < fItems.CountItems(); i++) {
		if (ItemAt(i)->Submenu() == submenu)
			return i;
	}

	return -1;
}


BMenuItem*
BMenu::FindItem(const char* label) const
{
	BMenuItem* item = NULL;

	for (int32 i = 0; i < CountItems(); i++) {
		item = ItemAt(i);

		if (item->Label() && strcmp(item->Label(), label) == 0)
			return item;

		if (item->Submenu() != NULL) {
			item = item->Submenu()->FindItem(label);
			if (item != NULL)
				return item;
		}
	}

	return NULL;
}


BMenuItem*
BMenu::FindItem(uint32 command) const
{
	BMenuItem* item = NULL;

	for (int32 i = 0; i < CountItems(); i++) {
		item = ItemAt(i);

		if (item->Command() == command)
			return item;

		if (item->Submenu() != NULL) {
			item = item->Submenu()->FindItem(command);
			if (item != NULL)
				return item;
		}
	}

	return NULL;
}


status_t
BMenu::SetTargetForItems(BHandler* handler)
{
	status_t status = B_OK;
	for (int32 i = 0; i < fItems.CountItems(); i++) {
		status = ItemAt(i)->SetTarget(handler);
		if (status < B_OK)
			break;
	}

	return status;
}


status_t
BMenu::SetTargetForItems(BMessenger messenger)
{
	status_t status = B_OK;
	for (int32 i = 0; i < fItems.CountItems(); i++) {
		status = ItemAt(i)->SetTarget(messenger);
		if (status < B_OK)
			break;
	}

	return status;
}


void
BMenu::SetEnabled(bool enable)
{
	if (fEnabled == enable)
		return;

	fEnabled = enable;

	if (dynamic_cast<_BMCMenuBar_*>(Supermenu()) != NULL)
		Supermenu()->SetEnabled(enable);

	if (fSuperitem)
		fSuperitem->SetEnabled(enable);
}


void
BMenu::SetRadioMode(bool on)
{
	fRadioMode = on;
	if (!on)
		SetLabelFromMarked(false);
}


void
BMenu::SetTriggersEnabled(bool enable)
{
	fTriggerEnabled = enable;
}


void
BMenu::SetMaxContentWidth(float width)
{
	fMaxContentWidth = width;
}


void
BMenu::SetLabelFromMarked(bool on)
{
	fDynamicName = on;
	if (on)
		SetRadioMode(true);
}


bool
BMenu::IsLabelFromMarked()
{
	return fDynamicName;
}


bool
BMenu::IsEnabled() const
{
	if (!fEnabled)
		return false;

	return fSuper ? fSuper->IsEnabled() : true ;
}


bool
BMenu::IsRadioMode() const
{
	return fRadioMode;
}


bool
BMenu::AreTriggersEnabled() const
{
	return fTriggerEnabled;
}


bool
BMenu::IsRedrawAfterSticky() const
{
	return false;
}


float
BMenu::MaxContentWidth() const
{
	return fMaxContentWidth;
}


BMenuItem*
BMenu::FindMarked()
{
	for (int32 i = 0; i < fItems.CountItems(); i++) {
		BMenuItem* item = ItemAt(i);

		if (item->IsMarked())
			return item;
	}

	return NULL;
}


int32
BMenu::FindMarkedIndex()
{
	for (int32 i = 0; i < fItems.CountItems(); i++) {
		BMenuItem* item = ItemAt(i);

		if (item->IsMarked())
			return i;
	}

	return -1;
}


BMenu*
BMenu::Supermenu() const
{
	return fSuper;
}


BMenuItem*
BMenu::Superitem() const
{
	return fSuperitem;
}


BHandler*
BMenu::ResolveSpecifier(BMessage* msg, int32 index, BMessage* specifier,
	int32 form, const char* property)
{
	BPropertyInfo propInfo(sPropList);
	BHandler* target = NULL;

	if (propInfo.FindMatch(msg, index, specifier, form, property) >= B_OK) {
		target = this;
	}

	if (!target)
		target = BView::ResolveSpecifier(msg, index, specifier, form,
		property);

	return target;
}


status_t
BMenu::GetSupportedSuites(BMessage* data)
{
	if (data == NULL)
		return B_BAD_VALUE;

	status_t err = data->AddString("suites", "suite/vnd.Be-menu");

	if (err < B_OK)
		return err;

	BPropertyInfo propertyInfo(sPropList);
	err = data->AddFlat("messages", &propertyInfo);

	if (err < B_OK)
		return err;

	return BView::GetSupportedSuites(data);
}


status_t
BMenu::Perform(perform_code code, void* _data)
{
	switch (code) {
		case PERFORM_CODE_MIN_SIZE:
			((perform_data_min_size*)_data)->return_value
				= BMenu::MinSize();
			return B_OK;

		case PERFORM_CODE_MAX_SIZE:
			((perform_data_max_size*)_data)->return_value
				= BMenu::MaxSize();
			return B_OK;

		case PERFORM_CODE_PREFERRED_SIZE:
			((perform_data_preferred_size*)_data)->return_value
				= BMenu::PreferredSize();
			return B_OK;

		case PERFORM_CODE_LAYOUT_ALIGNMENT:
			((perform_data_layout_alignment*)_data)->return_value
				= BMenu::LayoutAlignment();
			return B_OK;

		case PERFORM_CODE_HAS_HEIGHT_FOR_WIDTH:
			((perform_data_has_height_for_width*)_data)->return_value
				= BMenu::HasHeightForWidth();
			return B_OK;

		case PERFORM_CODE_GET_HEIGHT_FOR_WIDTH:
		{
			perform_data_get_height_for_width* data
				= (perform_data_get_height_for_width*)_data;
			BMenu::GetHeightForWidth(data->width, &data->min, &data->max,
				&data->preferred);
			return B_OK;
		}

		case PERFORM_CODE_SET_LAYOUT:
		{
			perform_data_set_layout* data = (perform_data_set_layout*)_data;
			BMenu::SetLayout(data->layout);
			return B_OK;
		}

		case PERFORM_CODE_LAYOUT_INVALIDATED:
		{
			perform_data_layout_invalidated* data
				= (perform_data_layout_invalidated*)_data;
			BMenu::LayoutInvalidated(data->descendants);
			return B_OK;
		}

		case PERFORM_CODE_DO_LAYOUT:
		{
			BMenu::DoLayout();
			return B_OK;
		}
	}

	return BView::Perform(code, _data);
}


// #pragma mark - BMenu protected methods


BMenu::BMenu(BRect frame, const char* name, uint32 resizingMode, uint32 flags,
	menu_layout layout, bool resizeToFit)
	:
	BView(frame, name, resizingMode, flags),
	fChosenItem(NULL),
	fSelected(NULL),
	fCachedMenuWindow(NULL),
	fSuper(NULL),
	fSuperitem(NULL),
	fAscent(-1.0f),
	fDescent(-1.0f),
	fFontHeight(-1.0f),
	fState(MENU_STATE_CLOSED),
	fLayout(layout),
	fExtraRect(NULL),
	fMaxContentWidth(0.0f),
	fInitMatrixSize(NULL),
	fExtraMenuData(NULL),
	fTrigger(0),
	fResizeToFit(resizeToFit),
	fUseCachedMenuLayout(false),
	fEnabled(true),
	fDynamicName(false),
	fRadioMode(false),
	fTrackNewBounds(false),
	fStickyMode(false),
	fIgnoreHidden(true),
	fTriggerEnabled(true),
	fHasSubmenus(false),
	fAttachAborted(false)
{
	_InitData(NULL);
}


void
BMenu::SetItemMargins(float left, float top, float right, float bottom)
{
	fPad.Set(left, top, right, bottom);
}


void
BMenu::GetItemMargins(float* _left, float* _top, float* _right,
	float* _bottom) const
{
	if (_left != NULL)
		*_left = fPad.left;

	if (_top != NULL)
		*_top = fPad.top;

	if (_right != NULL)
		*_right = fPad.right;

	if (_bottom != NULL)
		*_bottom = fPad.bottom;
}


menu_layout
BMenu::Layout() const
{
	return fLayout;
}


void
BMenu::Show()
{
	Show(false);
}


void
BMenu::Show(bool selectFirst)
{
	_Install(NULL);
	_Show(selectFirst);
}


void
BMenu::Hide()
{
	_Hide();
	_Uninstall();
}


BMenuItem*
BMenu::Track(bool sticky, BRect* clickToOpenRect)
{
	if (sticky && LockLooper()) {
		//RedrawAfterSticky(Bounds());
			// the call above didn't do anything, so I've removed it for now
		UnlockLooper();
	}

	if (clickToOpenRect != NULL && LockLooper()) {
		fExtraRect = clickToOpenRect;
		ConvertFromScreen(fExtraRect);
		UnlockLooper();
	}

	_SetStickyMode(sticky);

	int action;
	BMenuItem* menuItem = _Track(&action);

	fExtraRect = NULL;

	return menuItem;
}


// #pragma mark - BMenu private methods


bool
BMenu::AddDynamicItem(add_state state)
{
	// Implemented in subclasses
	return false;
}


void
BMenu::DrawBackground(BRect updateRect)
{
	rgb_color base = ui_color(B_MENU_BACKGROUND_COLOR);
	uint32 flags = 0;
	if (!IsEnabled())
		flags |= BControlLook::B_DISABLED;

	if (IsFocus())
		flags |= BControlLook::B_FOCUSED;

	BRect rect = Bounds();
	uint32 borders = BControlLook::B_LEFT_BORDER
		| BControlLook::B_RIGHT_BORDER;
	if (Window() != NULL && Parent() != NULL) {
		if (Parent()->Frame().top == Window()->Bounds().top)
			borders |= BControlLook::B_TOP_BORDER;

		if (Parent()->Frame().bottom == Window()->Bounds().bottom)
			borders |= BControlLook::B_BOTTOM_BORDER;
	} else {
		borders |= BControlLook::B_TOP_BORDER
			| BControlLook::B_BOTTOM_BORDER;
	}
	be_control_look->DrawMenuBackground(this, rect, updateRect, base, flags,
		borders);
}


void
BMenu::SetTrackingHook(menu_tracking_hook func, void* state)
{
	fExtraMenuData->trackingHook = func;
	fExtraMenuData->trackingState = state;
}


// #pragma mark - Reorder item methods


void
BMenu::SortItems(int (*compare)(const BMenuItem*, const BMenuItem*))
{
	BMenuItem** begin = (BMenuItem**)fItems.Items();
	BMenuItem** end = begin + fItems.CountItems();

	std::stable_sort(begin, end, BPrivate::MenuItemComparator(compare));

	InvalidateLayout();
	if (Window() != NULL && !Window()->IsHidden() && LockLooper()) {
		_LayoutItems(0);
		Invalidate();
		UnlockLooper();
	}
}


bool
BMenu::SwapItems(int32 indexA, int32 indexB)
{
	bool swapped = fItems.SwapItems(indexA, indexB);
	if (swapped) {
		InvalidateLayout();
		if (Window() != NULL && !Window()->IsHidden() && LockLooper()) {
			_LayoutItems(std::min(indexA, indexB));
			Invalidate();
			UnlockLooper();
		}
	}

	return swapped;
}


bool
BMenu::MoveItem(int32 indexFrom, int32 indexTo)
{
	bool moved = fItems.MoveItem(indexFrom, indexTo);
	if (moved) {
		InvalidateLayout();
		if (Window() != NULL && !Window()->IsHidden() && LockLooper()) {
			_LayoutItems(std::min(indexFrom, indexTo));
			Invalidate();
			UnlockLooper();
		}
	}

	return moved;
}


void BMenu::_ReservedMenu3() {}
void BMenu::_ReservedMenu4() {}
void BMenu::_ReservedMenu5() {}
void BMenu::_ReservedMenu6() {}


void
BMenu::_InitData(BMessage* archive)
{
	BPrivate::kEmptyMenuLabel = B_TRANSLATE("<empty>");

	// TODO: Get _color, _fname, _fflt from the message, if present
	BFont font;
	font.SetFamilyAndStyle(sMenuInfo.f_family, sMenuInfo.f_style);
	font.SetSize(sMenuInfo.font_size);
	SetFont(&font, B_FONT_FAMILY_AND_STYLE | B_FONT_SIZE);

	fExtraMenuData = new (nothrow) BPrivate::ExtraMenuData();

	const float labelSpacing = be_control_look->DefaultLabelSpacing();
	fPad = BRect(ceilf(labelSpacing * 2.3f), ceilf(labelSpacing / 3.0f),
		ceilf((labelSpacing / 3.0f) * 10.0f), 0.0f);

	fLayoutData = new LayoutData;
	fLayoutData->lastResizingMode = ResizingMode();

	SetLowUIColor(B_MENU_BACKGROUND_COLOR);
	SetViewColor(B_TRANSPARENT_COLOR);

	fTriggerEnabled = sMenuInfo.triggers_always_shown;

	if (archive != NULL) {
		archive->FindInt32("_layout", (int32*)&fLayout);
		archive->FindBool("_rsize_to_fit", &fResizeToFit);
		bool disabled;
		if (archive->FindBool("_disable", &disabled) == B_OK)
			fEnabled = !disabled;
		archive->FindBool("_radio", &fRadioMode);

		bool disableTrigger = false;
		archive->FindBool("_trig_disabled", &disableTrigger);
		fTriggerEnabled = !disableTrigger;

		archive->FindBool("_dyn_label", &fDynamicName);
		archive->FindFloat("_maxwidth", &fMaxContentWidth);

		BMessage msg;
		for (int32 i = 0; archive->FindMessage("_items", i, &msg) == B_OK; i++) {
			BArchivable* object = instantiate_object(&msg);
			if (BMenuItem* item = dynamic_cast<BMenuItem*>(object)) {
				BRect bounds;
				if (fLayout == B_ITEMS_IN_MATRIX
					&& archive->FindRect("_i_frames", i, &bounds) == B_OK)
					AddItem(item, bounds);
				else
					AddItem(item);
			}
		}
	}
}


bool
BMenu::_Show(bool selectFirstItem, bool keyDown)
{
	if (Window() != NULL)
		return false;

	// See if the supermenu has a cached menuwindow,
	// and use that one if possible.
	BMenuWindow* window = NULL;
	bool ourWindow = false;
	if (fSuper != NULL) {
		fSuperbounds = fSuper->ConvertToScreen(fSuper->Bounds());
		window = fSuper->_MenuWindow();
	}

	// Otherwise, create a new one
	// This happens for "stand alone" BPopUpMenus
	// (i.e. not within a BMenuField)
	if (window == NULL) {
		// Menu windows get the BMenu's handler name
		window = new (nothrow) BMenuWindow(Name());
		ourWindow = true;
	}

	if (window == NULL)
		return false;

	if (window->Lock()) {
		bool addAborted = false;
		if (keyDown)
			addAborted = _AddDynamicItems(keyDown);

		if (addAborted) {
			if (ourWindow)
				window->Quit();
			else
				window->Unlock();
			return false;
		}
		fAttachAborted = false;

		window->AttachMenu(this);

		if (ItemAt(0) != NULL) {
			float width, height;
			ItemAt(0)->GetContentSize(&width, &height);

			window->SetSmallStep(ceilf(height));
		}

		// Menu didn't have the time to add its items: aborting...
		if (fAttachAborted) {
			window->DetachMenu();
			// TODO: Probably not needed, we can just let _hide() quit the
			// window.
			if (ourWindow)
				window->Quit();
			else
				window->Unlock();
			return false;
		}

		_UpdateWindowViewSize(true);
		window->Show();

		if (selectFirstItem)
			_SelectItem(ItemAt(0), false);

		window->Unlock();
	}

	return true;
}


void
BMenu::_Hide()
{
	BMenuWindow* window = dynamic_cast<BMenuWindow*>(Window());
	if (window == NULL || !window->Lock())
		return;

	if (fSelected != NULL)
		_SelectItem(NULL);

	window->Hide();
	window->DetachMenu();
		// we don't want to be deleted when the window is removed

#if USE_CACHED_MENUWINDOW
	if (fSuper != NULL)
		window->Unlock();
	else
#endif
		window->Quit();
			// it's our window, quit it

	_DeleteMenuWindow();
		// Delete the menu window used by our submenus
}


void BMenu::_ScriptReceived(BMessage* message)
{
	BMessage replyMsg(B_REPLY);
	status_t err = B_BAD_SCRIPT_SYNTAX;
	int32 index;
	BMessage specifier;
	int32 what;
	const char* property;

	if (message->GetCurrentSpecifier(&index, &specifier, &what, &property)
			!= B_OK) {
		return BView::MessageReceived(message);
	}

	BPropertyInfo propertyInfo(sPropList);
	switch (propertyInfo.FindMatch(message, index, &specifier, what,
			property)) {
		case 0: // Enabled: GET
			if (message->what == B_GET_PROPERTY)
				err = replyMsg.AddBool("result", IsEnabled());
			break;
		case 1: // Enabled: SET
			if (message->what == B_SET_PROPERTY) {
				bool isEnabled;
				err = message->FindBool("data", &isEnabled);
				if (err >= B_OK)
					SetEnabled(isEnabled);
			}
			break;
		case 2: // Label: GET
		case 3: // Label: SET
		case 4: // Mark: GET
		case 5: { // Mark: SET
			BMenuItem *item = Superitem();
			if (item != NULL)
				return Supermenu()->_ItemScriptReceived(message, item);

			break;
		}
		case 6: // Menu: CREATE
			if (message->what == B_CREATE_PROPERTY) {
				const char *label;
				ObjectDeleter<BMessage> invokeMessage(new BMessage());
				BMessenger target;
				ObjectDeleter<BMenuItem> item;
				err = message->FindString("data", &label);
				if (err >= B_OK) {
					invokeMessage.SetTo(new BMessage());
					err = message->FindInt32("what",
						(int32*)&invokeMessage->what);
					if (err == B_NAME_NOT_FOUND) {
						invokeMessage.Unset();
						err = B_OK;
					}
				}
				if (err >= B_OK) {
					item.SetTo(new BMenuItem(new BMenu(label),
						invokeMessage.Detach()));
				}
				if (err >= B_OK) {
					err = _InsertItemAtSpecifier(specifier, what, item.Get());
				}
				if (err >= B_OK)
					item.Detach();
			}
			break;
		case 7: { // Menu: DELETE
			if (message->what == B_DELETE_PROPERTY) {
				BMenuItem *item = NULL;
				int32 index;
				err = _ResolveItemSpecifier(specifier, what, item, &index);
				if (err >= B_OK) {
					if (item->Submenu() == NULL)
						err = B_BAD_VALUE;
					else {
						if (index >= 0)
							RemoveItem(index);
						else
							RemoveItem(item);
					}
				}
			}
			break;
		}
		case 8: { // Menu: *
			// TODO: check that submenu looper is running and handle it
			// correctly
			BMenu *submenu = NULL;
			BMenuItem *item;
			err = _ResolveItemSpecifier(specifier, what, item);
			if (err >= B_OK)
				submenu = item->Submenu();
			if (submenu != NULL) {
				message->PopSpecifier();
				return submenu->_ScriptReceived(message);
			}
			break;
		}
		case 9: // MenuItem: COUNT
			if (message->what == B_COUNT_PROPERTIES)
				err = replyMsg.AddInt32("result", CountItems());
			break;
		case 10: // MenuItem: CREATE
			if (message->what == B_CREATE_PROPERTY) {
				const char *label;
				ObjectDeleter<BMessage> invokeMessage(new BMessage());
				bool targetPresent = true;
				BMessenger target;
				ObjectDeleter<BMenuItem> item;
				err = message->FindString("data", &label);
				if (err >= B_OK) {
					err = message->FindMessage("be:invoke_message",
						invokeMessage.Get());
					if (err == B_NAME_NOT_FOUND) {
						err = message->FindInt32("what",
							(int32*)&invokeMessage->what);
						if (err == B_NAME_NOT_FOUND) {
							invokeMessage.Unset();
							err = B_OK;
						}
					}
				}
				if (err >= B_OK) {
					err = message->FindMessenger("be:target", &target);
					if (err == B_NAME_NOT_FOUND) {
						targetPresent = false;
						err = B_OK;
					}
				}
				if (err >= B_OK) {
					item.SetTo(new BMenuItem(label, invokeMessage.Detach()));
					if (targetPresent)
						err = item->SetTarget(target);
				}
				if (err >= B_OK) {
					err = _InsertItemAtSpecifier(specifier, what, item.Get());
				}
				if (err >= B_OK)
					item.Detach();
			}
			break;
		case 11: // MenuItem: DELETE
			if (message->what == B_DELETE_PROPERTY) {
				BMenuItem *item = NULL;
				int32 index;
				err = _ResolveItemSpecifier(specifier, what, item, &index);
				if (err >= B_OK) {
					if (index >= 0)
						RemoveItem(index);
					else
						RemoveItem(item);
				}
			}
			break;
		case 12: { // MenuItem: EXECUTE
			if (message->what == B_EXECUTE_PROPERTY) {
				BMenuItem *item = NULL;
				err = _ResolveItemSpecifier(specifier, what, item);
				if (err >= B_OK) {
					if (!item->IsEnabled())
						err = B_NOT_ALLOWED;
					else
						err = item->Invoke();
				}
			}
			break;
		}
		case 13: { // MenuItem: *
			BMenuItem *item = NULL;
			err = _ResolveItemSpecifier(specifier, what, item);
			if (err >= B_OK) {
				message->PopSpecifier();
				return _ItemScriptReceived(message, item);
			}
			break;
		}
		default:
			return BView::MessageReceived(message);
	}

	if (err != B_OK) {
		replyMsg.what = B_MESSAGE_NOT_UNDERSTOOD;

		if (err == B_BAD_SCRIPT_SYNTAX)
			replyMsg.AddString("message", "Didn't understand the specifier(s)");
		else
			replyMsg.AddString("message", strerror(err));
	}

	replyMsg.AddInt32("error", err);
	message->SendReply(&replyMsg);
}


void BMenu::_ItemScriptReceived(BMessage* message, BMenuItem* item)
{
	BMessage replyMsg(B_REPLY);
	status_t err = B_BAD_SCRIPT_SYNTAX;
	int32 index;
	BMessage specifier;
	int32 what;
	const char* property;

	if (message->GetCurrentSpecifier(&index, &specifier, &what, &property)
			!= B_OK) {
		return BView::MessageReceived(message);
	}

	BPropertyInfo propertyInfo(sPropList);
	switch (propertyInfo.FindMatch(message, index, &specifier, what,
			property)) {
		case 0: // Enabled: GET
			if (message->what == B_GET_PROPERTY)
				err = replyMsg.AddBool("result", item->IsEnabled());
			break;
		case 1: // Enabled: SET
			if (message->what == B_SET_PROPERTY) {
				bool isEnabled;
				err = message->FindBool("data", &isEnabled);
				if (err >= B_OK)
					item->SetEnabled(isEnabled);
			}
			break;
		case 2: // Label: GET
			if (message->what == B_GET_PROPERTY)
				err = replyMsg.AddString("result", item->Label());
			break;
		case 3: // Label: SET
			if (message->what == B_SET_PROPERTY) {
				const char *label;
				err = message->FindString("data", &label);
				if (err >= B_OK)
					item->SetLabel(label);
			}
		case 4: // Mark: GET
			if (message->what == B_GET_PROPERTY)
				err = replyMsg.AddBool("result", item->IsMarked());
			break;
		case 5: // Mark: SET
			if (message->what == B_SET_PROPERTY) {
				bool isMarked;
				err = message->FindBool("data", &isMarked);
				if (err >= B_OK)
					item->SetMarked(isMarked);
			}
			break;
		case 6: // Menu: CREATE
		case 7: // Menu: DELETE
		case 8: // Menu: *
		case 9: // MenuItem: COUNT
		case 10: // MenuItem: CREATE
		case 11: // MenuItem: DELETE
		case 12: // MenuItem: EXECUTE
		case 13: // MenuItem: *
			break;
		default:
			return BView::MessageReceived(message);
	}

	if (err != B_OK) {
		replyMsg.what = B_MESSAGE_NOT_UNDERSTOOD;
		replyMsg.AddString("message", strerror(err));
	}

	replyMsg.AddInt32("error", err);
	message->SendReply(&replyMsg);
}


status_t BMenu::_ResolveItemSpecifier(const BMessage& specifier, int32 what,
	BMenuItem*& item, int32 *_index)
{
	status_t err;
	item = NULL;
	int32 index = -1;
	switch (what) {
		case B_INDEX_SPECIFIER:
		case B_REVERSE_INDEX_SPECIFIER: {
			err = specifier.FindInt32("index", &index);
			if (err < B_OK)
				return err;
			if (what == B_REVERSE_INDEX_SPECIFIER)
				index = CountItems() - index;
			item = ItemAt(index);
			break;
		}
		case B_NAME_SPECIFIER: {
			const char* name;
			err = specifier.FindString("name", &name);
			if (err < B_OK)
				return err;
			item = FindItem(name);
			break;
		}
	}
	if (item == NULL)
		return B_BAD_INDEX;

	if (_index != NULL)
		*_index = index;

	return B_OK;
}


status_t BMenu::_InsertItemAtSpecifier(const BMessage& specifier, int32 what,
	BMenuItem* item)
{
	status_t err;
	switch (what) {
		case B_INDEX_SPECIFIER:
		case B_REVERSE_INDEX_SPECIFIER: {
			int32 index;
			err = specifier.FindInt32("index", &index);
			if (err < B_OK) return err;
			if (what == B_REVERSE_INDEX_SPECIFIER)
				index = CountItems() - index;
			if (!AddItem(item, index))
				return B_BAD_INDEX;
			break;
		}
		case B_NAME_SPECIFIER:
			return B_NOT_SUPPORTED;
			break;
	}

	return B_OK;
}


// #pragma mark - mouse tracking


const static bigtime_t kOpenSubmenuDelay = 0;
const static bigtime_t kNavigationAreaTimeout = 1000000;


BMenuItem*
BMenu::_Track(int* action, long start)
{
	// TODO: cleanup
	BMenuItem* item = NULL;
	BRect navAreaRectAbove;
	BRect navAreaRectBelow;
	bigtime_t selectedTime = system_time();
	bigtime_t navigationAreaTime = 0;

	fState = MENU_STATE_TRACKING;
	fChosenItem = NULL;
		// we will use this for keyboard selection

	BPoint location;
	uint32 buttons = 0;
	if (LockLooper()) {
		GetMouse(&location, &buttons);
		UnlockLooper();
	}

	bool releasedOnce = buttons == 0;
	while (fState != MENU_STATE_CLOSED) {
		if (_CustomTrackingWantsToQuit())
			break;

		if (!LockLooper())
			break;

		BMenuWindow* window = static_cast<BMenuWindow*>(Window());
		BPoint screenLocation = ConvertToScreen(location);
		if (window->CheckForScrolling(screenLocation)) {
			UnlockLooper();
			continue;
		}

		// The order of the checks is important
		// to be able to handle overlapping menus:
		// first we check if mouse is inside a submenu,
		// then if the mouse is inside this menu,
		// then if it's over a super menu.
		if (_OverSubmenu(fSelected, screenLocation)
			|| fState == MENU_STATE_KEY_TO_SUBMENU) {
			if (fState == MENU_STATE_TRACKING) {
				// not if from R.Arrow
				fState = MENU_STATE_TRACKING_SUBMENU;
			}
			navAreaRectAbove = BRect();
			navAreaRectBelow = BRect();

			// Since the submenu has its own looper,
			// we can unlock ours. Doing so also make sure
			// that our window gets any update message to
			// redraw itself
			UnlockLooper();

			// To prevent NULL access violation, ensure a menu has actually
			// been selected and that it has a submenu. Because keyboard and
			// mouse interactions set selected items differently, the menu
			// tracking thread needs to be careful in triggering the navigation
			// to the submenu.
			if (fSelected != NULL) {
				BMenu* submenu = fSelected->Submenu();
				int submenuAction = MENU_STATE_TRACKING;
				if (submenu != NULL) {
					submenu->_SetStickyMode(_IsStickyMode());

					// The following call blocks until the submenu
					// gives control back to us, either because the mouse
					// pointer goes out of the submenu's bounds, or because
					// the user closes the menu
					BMenuItem* submenuItem = submenu->_Track(&submenuAction);
					if (submenuAction == MENU_STATE_CLOSED) {
						item = submenuItem;
						fState = MENU_STATE_CLOSED;
					} else if (submenuAction == MENU_STATE_KEY_LEAVE_SUBMENU) {
						if (LockLooper()) {
							BMenuItem* temp = fSelected;
							// close the submenu:
							_SelectItem(NULL);
							// but reselect the item itself for user:
							_SelectItem(temp, false);
							UnlockLooper();
						}
						// cancel  key-nav state
						fState = MENU_STATE_TRACKING;
					} else
						fState = MENU_STATE_TRACKING;
				}
			}
			if (!LockLooper())
				break;
		} else if ((item = _HitTestItems(location, B_ORIGIN)) != NULL) {
			_UpdateStateOpenSelect(item, location, navAreaRectAbove,
				navAreaRectBelow, selectedTime, navigationAreaTime);
			releasedOnce = true;
		} else if (_OverSuper(screenLocation)
			&& fSuper->fState != MENU_STATE_KEY_TO_SUBMENU) {
			fState = MENU_STATE_TRACKING;
			UnlockLooper();
			break;
		} else if (fState == MENU_STATE_KEY_LEAVE_SUBMENU) {
			UnlockLooper();
			break;
		} else if (fSuper == NULL
			|| fSuper->fState != MENU_STATE_KEY_TO_SUBMENU) {
			// Mouse pointer outside menu:
			// If there's no other submenu opened,
			// deselect the current selected item
			if (fSelected != NULL
				&& (fSelected->Submenu() == NULL
					|| fSelected->Submenu()->Window() == NULL)) {
				_SelectItem(NULL);
				fState = MENU_STATE_TRACKING;
			}

			if (fSuper != NULL) {
				// Give supermenu the chance to continue tracking
				*action = fState;
				UnlockLooper();
				return NULL;
			}
		}

		UnlockLooper();

		if (releasedOnce)
			_UpdateStateClose(item, location, buttons);

		if (fState != MENU_STATE_CLOSED) {
			bigtime_t snoozeAmount = 50000;

			BPoint newLocation = location;
			uint32 newButtons = buttons;

			// If user doesn't move the mouse, loop here,
			// so we don't interfere with keyboard menu navigation
			do {
				snooze(snoozeAmount);
				if (!LockLooper())
					break;
				GetMouse(&newLocation, &newButtons, true);
				UnlockLooper();
			} while (newLocation == location && newButtons == buttons
				&& !(item != NULL && item->Submenu() != NULL
					&& item->Submenu()->Window() == NULL)
				&& fState == MENU_STATE_TRACKING);

			if (newLocation != location || newButtons != buttons) {
				if (!releasedOnce && newButtons == 0 && buttons != 0)
					releasedOnce = true;
				location = newLocation;
				buttons = newButtons;
			}

			if (releasedOnce)
				_UpdateStateClose(item, location, buttons);
		}
	}

	if (action != NULL)
		*action = fState;

	// keyboard Enter will set this
	if (fChosenItem != NULL)
		item = fChosenItem;
	else if (fSelected == NULL) {
		// needed to cover (rare) mouse/ESC combination
		item = NULL;
	}

	if (fSelected != NULL && LockLooper()) {
		_SelectItem(NULL);
		UnlockLooper();
	}

	// delete the menu window recycled for all the child menus
	_DeleteMenuWindow();

	return item;
}


void
BMenu::_UpdateNavigationArea(BPoint position, BRect& navAreaRectAbove,
	BRect& navAreaRectBelow)
{
#define NAV_AREA_THRESHOLD    8

	// The navigation area is a region in which mouse-overs won't select
	// the item under the cursor. This makes it easier to navigate to
	// submenus, as the cursor can be moved to submenu items directly instead
	// of having to move it horizontally into the submenu first. The concept
	// is illustrated below:
	//
	// +-------+----+---------+
	// |       |   /|         |
	// |       |  /*|         |
	// |[2]--> | /**|         |
	// |       |/[4]|         |
	// |------------|         |
	// |    [1]     |   [6]   |
	// |------------|         |
	// |       |\[5]|         |
	// |[3]--> | \**|         |
	// |       |  \*|         |
	// |       |   \|         |
	// |       +----|---------+
	// |            |
	// +------------+
	//
	// [1] Selected item, cursor position ('position')
	// [2] Upper navigation area rectangle ('navAreaRectAbove')
	// [3] Lower navigation area rectangle ('navAreaRectBelow')
	// [4] Upper navigation area
	// [5] Lower navigation area
	// [6] Submenu
	//
	// The rectangles are used to calculate if the cursor is in the actual
	// navigation area (see _UpdateStateOpenSelect()).

	if (fSelected == NULL)
		return;

	BMenu* submenu = fSelected->Submenu();

	if (submenu != NULL) {
		BRect menuBounds = ConvertToScreen(Bounds());

		BRect submenuBounds;
		if (fSelected->Submenu()->LockLooper()) {
			submenuBounds = fSelected->Submenu()->ConvertToScreen(
				fSelected->Submenu()->Bounds());
			fSelected->Submenu()->UnlockLooper();
		}

		if (menuBounds.left < submenuBounds.left) {
			navAreaRectAbove.Set(position.x + NAV_AREA_THRESHOLD,
				submenuBounds.top, menuBounds.right,
				position.y);
			navAreaRectBelow.Set(position.x + NAV_AREA_THRESHOLD,
				position.y, menuBounds.right,
				submenuBounds.bottom);
		} else {
			navAreaRectAbove.Set(menuBounds.left,
				submenuBounds.top, position.x - NAV_AREA_THRESHOLD,
				position.y);
			navAreaRectBelow.Set(menuBounds.left,
				position.y, position.x - NAV_AREA_THRESHOLD,
				submenuBounds.bottom);
		}
	} else {
		navAreaRectAbove = BRect();
		navAreaRectBelow = BRect();
	}
}


void
BMenu::_UpdateStateOpenSelect(BMenuItem* item, BPoint position,
	BRect& navAreaRectAbove, BRect& navAreaRectBelow, bigtime_t& selectedTime,
	bigtime_t& navigationAreaTime)
{
	if (fState == MENU_STATE_CLOSED)
		return;

	if (item != fSelected) {
		if (navigationAreaTime == 0)
			navigationAreaTime = system_time();

		position = ConvertToScreen(position);

		bool inNavAreaRectAbove = navAreaRectAbove.Contains(position);
		bool inNavAreaRectBelow = navAreaRectBelow.Contains(position);

		if (fSelected == NULL
			|| (!inNavAreaRectAbove && !inNavAreaRectBelow)) {
			_SelectItem(item, false);
			navAreaRectAbove = BRect();
			navAreaRectBelow = BRect();
			selectedTime = system_time();
			navigationAreaTime = 0;
			return;
		}

		bool isLeft = ConvertFromScreen(navAreaRectAbove).left == 0;
		BPoint p1, p2;

		if (inNavAreaRectAbove) {
			if (!isLeft) {
				p1 = navAreaRectAbove.LeftBottom();
				p2 = navAreaRectAbove.RightTop();
			} else {
				p2 = navAreaRectAbove.RightBottom();
				p1 = navAreaRectAbove.LeftTop();
			}
		} else {
			if (!isLeft) {
				p2 = navAreaRectBelow.LeftTop();
				p1 = navAreaRectBelow.RightBottom();
			} else {
				p1 = navAreaRectBelow.RightTop();
				p2 = navAreaRectBelow.LeftBottom();
			}
		}
		bool inNavArea =
			  (p1.y - p2.y) * position.x + (p2.x - p1.x) * position.y
			+ (p1.x - p2.x) * p1.y + (p2.y - p1.y) * p1.x >= 0;

		bigtime_t systime = system_time();

		if (!inNavArea || (navigationAreaTime > 0 && systime -
			navigationAreaTime > kNavigationAreaTimeout)) {
			// Don't delay opening of submenu if the user had
			// to wait for the navigation area timeout anyway
			_SelectItem(item, inNavArea);

			if (inNavArea) {
				_UpdateNavigationArea(position, navAreaRectAbove,
					navAreaRectBelow);
			} else {
				navAreaRectAbove = BRect();
				navAreaRectBelow = BRect();
			}

			selectedTime = system_time();
			navigationAreaTime = 0;
		}
	} else if (fSelected->Submenu() != NULL &&
		system_time() - selectedTime > kOpenSubmenuDelay) {
		_SelectItem(fSelected, true);

		if (!navAreaRectAbove.IsValid() && !navAreaRectBelow.IsValid()) {
			position = ConvertToScreen(position);
			_UpdateNavigationArea(position, navAreaRectAbove,
				navAreaRectBelow);
		}
	}

	if (fState != MENU_STATE_TRACKING)
		fState = MENU_STATE_TRACKING;
}


void
BMenu::_UpdateStateClose(BMenuItem* item, const BPoint& where,
	const uint32& buttons)
{
	if (fState == MENU_STATE_CLOSED)
		return;

	if (buttons != 0 && _IsStickyMode()) {
		if (item == NULL) {
			if (item != fSelected && LockLooper()) {
				_SelectItem(item, false);
				UnlockLooper();
			}
			fState = MENU_STATE_CLOSED;
		} else
			_SetStickyMode(false);
	} else if (buttons == 0 && !_IsStickyMode()) {
		if (fExtraRect != NULL && fExtraRect->Contains(where)) {
			_SetStickyMode(true);
			fExtraRect = NULL;
				// Setting this to NULL will prevent this code
				// to be executed next time
		} else {
			if (item != fSelected && LockLooper()) {
				_SelectItem(item, false);
				UnlockLooper();
			}
			fState = MENU_STATE_CLOSED;
		}
	}
}


bool
BMenu::_AddItem(BMenuItem* item, int32 index)
{
	ASSERT(item != NULL);
	if (index < 0 || index > fItems.CountItems())
		return false;

	if (item->IsMarked())
		_ItemMarked(item);

	if (!fItems.AddItem(item, index))
		return false;

	// install the item on the supermenu's window
	// or onto our window, if we are a root menu
	BWindow* window = NULL;
	if (Superitem() != NULL)
		window = Superitem()->fWindow;
	else
		window = Window();
	if (window != NULL)
		item->Install(window);

	item->SetSuper(this);
	return true;
}


bool
BMenu::_RemoveItems(int32 index, int32 count, BMenuItem* item,
	bool deleteItems)
{
	bool success = false;
	bool invalidateLayout = false;

	bool locked = LockLooper();
	BWindow* window = Window();

	// The plan is simple: If we're given a BMenuItem directly, we use it
	// and ignore index and count. Otherwise, we use them instead.
	if (item != NULL) {
		if (fItems.RemoveItem(item)) {
			if (item == fSelected && window != NULL)
				_SelectItem(NULL);
			item->Uninstall();
			item->SetSuper(NULL);
			if (deleteItems)
				delete item;
			success = invalidateLayout = true;
		}
	} else {
		// We iterate backwards because it's simpler
		int32 i = std::min(index + count - 1, fItems.CountItems() - 1);
		// NOTE: the range check for "index" is done after
		// calculating the last index to be removed, so
		// that the range is not "shifted" unintentionally
		index = std::max((int32)0, index);
		for (; i >= index; i--) {
			item = static_cast<BMenuItem*>(fItems.ItemAt(i));
			if (item != NULL) {
				if (fItems.RemoveItem(i)) {
					if (item == fSelected && window != NULL)
						_SelectItem(NULL);
					item->Uninstall();
					item->SetSuper(NULL);
					if (deleteItems)
						delete item;
					success = true;
					invalidateLayout = true;
				} else {
					// operation not entirely successful
					success = false;
					break;
				}
			}
		}
	}

	if (invalidateLayout) {
		InvalidateLayout();
		if (locked && window != NULL) {
			_LayoutItems(0);
			_UpdateWindowViewSize(false);
			Invalidate();
		}
	}

	if (locked)
		UnlockLooper();

	return success;
}


bool
BMenu::_RelayoutIfNeeded()
{
	if (!fUseCachedMenuLayout) {
		fUseCachedMenuLayout = true;
		_CacheFontInfo();
		_LayoutItems(0);
		_UpdateWindowViewSize(false);
		return true;
	}
	return false;
}


void
BMenu::_LayoutItems(int32 index)
{
	_CalcTriggers();

	float width;
	float height;
	_ComputeLayout(index, fResizeToFit, true, &width, &height);

	if (fResizeToFit)
		ResizeTo(width, height);
}


BSize
BMenu::_ValidatePreferredSize()
{
	if (!fLayoutData->preferred.IsWidthSet() || ResizingMode()
			!= fLayoutData->lastResizingMode) {
		_ComputeLayout(0, true, false, NULL, NULL);
		ResetLayoutInvalidation();
	}

	return fLayoutData->preferred;
}


void
BMenu::_ComputeLayout(int32 index, bool bestFit, bool moveItems,
	float* _width, float* _height)
{
	// TODO: Take "bestFit", "moveItems", "index" into account,
	// Recalculate only the needed items,
	// not the whole layout every time

	fLayoutData->lastResizingMode = ResizingMode();

	BRect frame;
	switch (fLayout) {
		case B_ITEMS_IN_COLUMN:
		{
			BRect parentFrame;
			BRect* overrideFrame = NULL;
			if (dynamic_cast<_BMCMenuBar_*>(Supermenu()) != NULL) {
				// When the menu is modified while it's open, we get here in a
				// situation where trying to lock the looper would deadlock
				// (the window is locked waiting for the menu to terminate).
				// In that case, just give up on getting the supermenu bounds
				// and keep the menu at the current width and position.
				if (Supermenu()->LockLooperWithTimeout(0) == B_OK) {
					parentFrame = Supermenu()->Bounds();
					Supermenu()->UnlockLooper();
					overrideFrame = &parentFrame;
				}
			}

			_ComputeColumnLayout(index, bestFit, moveItems, overrideFrame,
				frame);
			break;
		}

		case B_ITEMS_IN_ROW:
			_ComputeRowLayout(index, bestFit, moveItems, frame);
			break;

		case B_ITEMS_IN_MATRIX:
			_ComputeMatrixLayout(frame);
			break;
	}

	// change width depending on resize mode
	BSize size;
	if ((ResizingMode() & B_FOLLOW_LEFT_RIGHT) == B_FOLLOW_LEFT_RIGHT) {
		if (dynamic_cast<_BMCMenuBar_*>(this) != NULL)
			size.width = Bounds().Width() - fPad.right;
		else if (Parent() != NULL)
			size.width = Parent()->Frame().Width();
		else if (Window() != NULL)
			size.width = Window()->Frame().Width();
		else
			size.width = Bounds().Width();
	} else
		size.width = frame.Width();

	size.height = frame.Height();

	if (_width)
		*_width = size.width;

	if (_height)
		*_height = size.height;

	if (bestFit)
		fLayoutData->preferred = size;

	if (moveItems)
		fUseCachedMenuLayout = true;
}


void
BMenu::_ComputeColumnLayout(int32 index, bool bestFit, bool moveItems,
	BRect* overrideFrame, BRect& frame)
{
	bool command = false;
	bool control = false;
	bool shift = false;
	bool option = false;
	bool submenu = false;

	if (index > 0)
		frame = ItemAt(index - 1)->Frame();
	else if (overrideFrame != NULL)
		frame.Set(0, 0, overrideFrame->right, -1);
	else
		frame.Set(0, 0, 0, -1);

	BFont font;
	GetFont(&font);

	// Loop over all items to set their top, bottom and left coordinates,
	// all while computing the width of the menu
	for (; index < fItems.CountItems(); index++) {
		BMenuItem* item = ItemAt(index);

		float width;
		float height;
		item->GetContentSize(&width, &height);

		if (item->fModifiers && item->fShortcutChar) {
			width += font.Size();
			if ((item->fModifiers & B_COMMAND_KEY) != 0)
				command = true;

			if ((item->fModifiers & B_CONTROL_KEY) != 0)
				control = true;

			if ((item->fModifiers & B_SHIFT_KEY) != 0)
				shift = true;

			if ((item->fModifiers & B_OPTION_KEY) != 0)
				option = true;
		}

		item->fBounds.left = 0.0f;
		item->fBounds.top = frame.bottom + 1.0f;
		item->fBounds.bottom = item->fBounds.top + height + fPad.top
			+ fPad.bottom;

		if (item->fSubmenu != NULL)
			submenu = true;

		frame.right = std::max(frame.right, width + fPad.left + fPad.right);
		frame.bottom = item->fBounds.bottom;
	}

	// Compute the extra space needed for shortcuts and submenus
	if (command) {
		frame.right
			+= BPrivate::MenuPrivate::MenuItemCommand()->Bounds().Width() + 1;
	}
	if (control) {
		frame.right
			+= BPrivate::MenuPrivate::MenuItemControl()->Bounds().Width() + 1;
	}
	if (option) {
		frame.right
			+= BPrivate::MenuPrivate::MenuItemOption()->Bounds().Width() + 1;
	}
	if (shift) {
		frame.right
			+= BPrivate::MenuPrivate::MenuItemShift()->Bounds().Width() + 1;
	}
	if (submenu) {
		frame.right += ItemAt(0)->Frame().Height() / 2;
		fHasSubmenus = true;
	} else {
		fHasSubmenus = false;
	}

	if (fMaxContentWidth > 0)
		frame.right = std::min(frame.right, fMaxContentWidth);

	frame.top = 0;
	frame.right = ceilf(frame.right);

	// Finally update the "right" coordinate of all items
	if (moveItems) {
		for (int32 i = 0; i < fItems.CountItems(); i++)
			ItemAt(i)->fBounds.right = frame.right;
	}
}


void
BMenu::_ComputeRowLayout(int32 index, bool bestFit, bool moveItems,
	BRect& frame)
{
	font_height fh;
	GetFontHeight(&fh);
	frame.Set(0.0f, 0.0f, 0.0f, ceilf(fh.ascent + fh.descent + fPad.top
		+ fPad.bottom));

	for (int32 i = 0; i < fItems.CountItems(); i++) {
		BMenuItem* item = ItemAt(i);

		float width, height;
		item->GetContentSize(&width, &height);

		item->fBounds.left = frame.right;
		item->fBounds.top = 0.0f;
		item->fBounds.right = item->fBounds.left + width + fPad.left
			+ fPad.right;

		frame.right = item->Frame().right + 1.0f;
		frame.bottom = std::max(frame.bottom, height + fPad.top + fPad.bottom);
	}

	if (moveItems) {
		for (int32 i = 0; i < fItems.CountItems(); i++)
			ItemAt(i)->fBounds.bottom = frame.bottom;
	}

	if (bestFit)
		frame.right = ceilf(frame.right);
	else
		frame.right = Bounds().right;
}


void
BMenu::_ComputeMatrixLayout(BRect &frame)
{
	frame.Set(0, 0, 0, 0);
	for (int32 i = 0; i < CountItems(); i++) {
		BMenuItem* item = ItemAt(i);
		if (item != NULL) {
			frame.left = std::min(frame.left, item->Frame().left);
			frame.right = std::max(frame.right, item->Frame().right);
			frame.top = std::min(frame.top, item->Frame().top);
			frame.bottom = std::max(frame.bottom, item->Frame().bottom);
		}
	}
}


void
BMenu::LayoutInvalidated(bool descendants)
{
	fUseCachedMenuLayout = false;
	fLayoutData->preferred.Set(B_SIZE_UNSET, B_SIZE_UNSET);
}


// Assumes the SuperMenu to be locked (due to calling ConvertToScreen())
BPoint
BMenu::ScreenLocation()
{
	BMenu* superMenu = Supermenu();
	BMenuItem* superItem = Superitem();

	if (superMenu == NULL || superItem == NULL) {
		debugger("BMenu can't determine where to draw."
			"Override BMenu::ScreenLocation() to determine location.");
	}

	BPoint point;
	if (superMenu->Layout() == B_ITEMS_IN_COLUMN)
		point = superItem->Frame().RightTop() + BPoint(1.0f, 1.0f);
	else
		point = superItem->Frame().LeftBottom() + BPoint(1.0f, 1.0f);

	superMenu->ConvertToScreen(&point);

	return point;
}


BRect
BMenu::_CalcFrame(BPoint where, bool* scrollOn)
{
	// TODO: Improve me
	BRect bounds = Bounds();
	BRect frame = bounds.OffsetToCopy(where);

	BScreen screen(Window());
	BRect screenFrame = screen.Frame();

	BMenu* superMenu = Supermenu();
	BMenuItem* superItem = Superitem();

	// Reset frame shifted state since this menu is being redrawn
	fExtraMenuData->frameShiftedLeft = false;

	// TODO: Horrible hack:
	// When added to a BMenuField, a BPopUpMenu is the child of
	// a _BMCMenuBar_ to "fake" the menu hierarchy
	bool inMenuField = dynamic_cast<_BMCMenuBar_*>(superMenu) != NULL;

	// Offset the menu field menu window left by the width of the checkmark
	// so that the text when the menu is closed lines up with the text when
	// the menu is open.
	if (inMenuField)
		frame.OffsetBy(-8.0f, 0.0f);

	if (superMenu == NULL || superItem == NULL || inMenuField) {
		// just move the window on screen
		if (frame.bottom > screenFrame.bottom)
			frame.OffsetBy(0, screenFrame.bottom - frame.bottom);
		else if (frame.top < screenFrame.top)
			frame.OffsetBy(0, -frame.top);

		if (frame.right > screenFrame.right) {
			frame.OffsetBy(screenFrame.right - frame.right, 0);
			fExtraMenuData->frameShiftedLeft = true;
		}
		else if (frame.left < screenFrame.left)
			frame.OffsetBy(-frame.left, 0);
	} else if (superMenu->Layout() == B_ITEMS_IN_COLUMN) {
		if (frame.right > screenFrame.right
				|| superMenu->fExtraMenuData->frameShiftedLeft) {
			frame.OffsetBy(-superItem->Frame().Width() - frame.Width() - 2, 0);
			fExtraMenuData->frameShiftedLeft = true;
		}

		if (frame.left < 0)
			frame.OffsetBy(-frame.left + 6, 0);

		if (frame.bottom > screenFrame.bottom)
			frame.OffsetBy(0, screenFrame.bottom - frame.bottom);
	} else {
		if (frame.bottom > screenFrame.bottom) {
			float spaceBelow = screenFrame.bottom - frame.top;
			float spaceOver = frame.top - screenFrame.top
				- superItem->Frame().Height();
			if (spaceOver > spaceBelow) {
				frame.OffsetBy(0, -superItem->Frame().Height()
					- frame.Height() - 3);
			}
		}

		if (frame.right > screenFrame.right)
			frame.OffsetBy(screenFrame.right - frame.right, 0);
	}

	if (scrollOn != NULL) {
		// basically, if this returns false, it means
		// that the menu frame won't fit completely inside the screen
		// TODO: Scrolling will currently only work up/down,
		// not left/right
		*scrollOn = screenFrame.top > frame.top
			|| screenFrame.bottom < frame.bottom;
	}

	return frame;
}


void
BMenu::DrawItems(BRect updateRect)
{
	int32 itemCount = fItems.CountItems();
	for (int32 i = 0; i < itemCount; i++) {
		BMenuItem* item = ItemAt(i);
		if (item->Frame().Intersects(updateRect))
			item->Draw();
	}
}


int
BMenu::_State(BMenuItem** item) const
{
	if (fState == MENU_STATE_TRACKING || fState == MENU_STATE_CLOSED)
		return fState;

	if (fSelected != NULL && fSelected->Submenu() != NULL)
		return fSelected->Submenu()->_State(item);

	return fState;
}


void
BMenu::_InvokeItem(BMenuItem* item, bool now)
{
	if (!item->IsEnabled())
		return;

	// Do the "selected" animation
	// TODO: Doesn't work. This is supposed to highlight
	// and dehighlight the item, works on beos but not on haiku.
	if (!item->Submenu() && LockLooper()) {
		snooze(50000);
		item->Select(true);
		Window()->UpdateIfNeeded();
		snooze(50000);
		item->Select(false);
		Window()->UpdateIfNeeded();
		snooze(50000);
		item->Select(true);
		Window()->UpdateIfNeeded();
		snooze(50000);
		item->Select(false);
		Window()->UpdateIfNeeded();
		UnlockLooper();
	}

	// Lock the root menu window before calling BMenuItem::Invoke()
	BMenu* parent = this;
	BMenu* rootMenu = NULL;
	do {
		rootMenu = parent;
		parent = rootMenu->Supermenu();
	} while (parent != NULL);

	if (rootMenu->LockLooper()) {
		item->Invoke();
		rootMenu->UnlockLooper();
	}
}


bool
BMenu::_OverSuper(BPoint location)
{
	if (!Supermenu())
		return false;

	return fSuperbounds.Contains(location);
}


bool
BMenu::_OverSubmenu(BMenuItem* item, BPoint loc)
{
	if (item == NULL)
		return false;

	BMenu* subMenu = item->Submenu();
	if (subMenu == NULL || subMenu->Window() == NULL)
		return false;

	// assume that loc is in screen coordinates
	if (subMenu->Window()->Frame().Contains(loc))
		return true;

	return subMenu->_OverSubmenu(subMenu->fSelected, loc);
}


BMenuWindow*
BMenu::_MenuWindow()
{
#if USE_CACHED_MENUWINDOW
	if (fCachedMenuWindow == NULL) {
		char windowName[64];
		snprintf(windowName, 64, "%s cached menu", Name());
		fCachedMenuWindow = new (nothrow) BMenuWindow(windowName);
	}
#endif
	return fCachedMenuWindow;
}


void
BMenu::_DeleteMenuWindow()
{
	if (fCachedMenuWindow != NULL) {
		fCachedMenuWindow->Lock();
		fCachedMenuWindow->Quit();
		fCachedMenuWindow = NULL;
	}
}


BMenuItem*
BMenu::_HitTestItems(BPoint where, BPoint slop) const
{
	// TODO: Take "slop" into account ?

	// if the point doesn't lie within the menu's
	// bounds, bail out immediately
	if (!Bounds().Contains(where))
		return NULL;

	int32 itemCount = CountItems();
	for (int32 i = 0; i < itemCount; i++) {
		BMenuItem* item = ItemAt(i);
		if (item->Frame().Contains(where)
			&& dynamic_cast<BSeparatorItem*>(item) == NULL) {
			return item;
		}
	}

	return NULL;
}


BRect
BMenu::_Superbounds() const
{
	return fSuperbounds;
}


void
BMenu::_CacheFontInfo()
{
	font_height fh;
	GetFontHeight(&fh);
	fAscent = fh.ascent;
	fDescent = fh.descent;
	fFontHeight = ceilf(fh.ascent + fh.descent + fh.leading);
}


void
BMenu::_ItemMarked(BMenuItem* item)
{
	if (IsRadioMode()) {
		for (int32 i = 0; i < CountItems(); i++) {
			if (ItemAt(i) != item)
				ItemAt(i)->SetMarked(false);
		}
	}

	if (IsLabelFromMarked() && Superitem() != NULL)
		Superitem()->SetLabel(item->Label());
}


void
BMenu::_Install(BWindow* target)
{
	for (int32 i = 0; i < CountItems(); i++)
		ItemAt(i)->Install(target);
}


void
BMenu::_Uninstall()
{
	for (int32 i = 0; i < CountItems(); i++)
		ItemAt(i)->Uninstall();
}


void
BMenu::_SelectItem(BMenuItem* item, bool showSubmenu, bool selectFirstItem,
	bool keyDown)
{
	// Avoid deselecting and then reselecting the same item
	// which would cause flickering
	if (item != fSelected) {
		if (fSelected != NULL) {
			fSelected->Select(false);
			BMenu* subMenu = fSelected->Submenu();
			if (subMenu != NULL && subMenu->Window() != NULL)
				subMenu->_Hide();
		}

		fSelected = item;
		if (fSelected != NULL)
			fSelected->Select(true);
	}

	if (fSelected != NULL && showSubmenu) {
		BMenu* subMenu = fSelected->Submenu();
		if (subMenu != NULL && subMenu->Window() == NULL) {
			if (!subMenu->_Show(selectFirstItem, keyDown)) {
				// something went wrong, deselect the item
				fSelected->Select(false);
				fSelected = NULL;
			}
		}
	}
}


bool
BMenu::_SelectNextItem(BMenuItem* item, bool forward)
{
	if (CountItems() == 0) // cannot select next item in an empty menu
		return false;

	BMenuItem* nextItem = _NextItem(item, forward);
	if (nextItem == NULL)
		return false;

	_SelectItem(nextItem, dynamic_cast<BMenuBar*>(this) != NULL);

	if (LockLooper()) {
		be_app->ObscureCursor();
		UnlockLooper();
	}

	return true;
}


BMenuItem*
BMenu::_NextItem(BMenuItem* item, bool forward) const
{
	const int32 numItems = fItems.CountItems();
	if (numItems == 0)
		return NULL;

	int32 index = fItems.IndexOf(item);
	int32 loopCount = numItems;
	while (--loopCount) {
		// Cycle through menu items in the given direction...
		if (forward)
			index++;
		else
			index--;

		// ... wrap around...
		if (index < 0)
			index = numItems - 1;
		else if (index >= numItems)
			index = 0;

		// ... and return the first suitable item found.
		BMenuItem* nextItem = ItemAt(index);
		if (nextItem->IsEnabled())
			return nextItem;
	}

	// If no other suitable item was found, return NULL.
	return NULL;
}


void
BMenu::_SetStickyMode(bool sticky)
{
	if (fStickyMode == sticky)
		return;

	fStickyMode = sticky;

	if (fSuper != NULL) {
		// propagate the status to the super menu
		fSuper->_SetStickyMode(sticky);
	} else {
		// TODO: Ugly hack, but it needs to be done in this method
		BMenuBar* menuBar = dynamic_cast<BMenuBar*>(this);
		if (sticky && menuBar != NULL && menuBar->LockLooper()) {
			// If we are switching to sticky mode,
			// steal the focus from the current focus view
			// (needed to handle keyboard navigation)
			menuBar->_StealFocus();
			menuBar->UnlockLooper();
		}
	}
}


bool
BMenu::_IsStickyMode() const
{
	return fStickyMode;
}


void
BMenu::_GetShiftKey(uint32 &value) const
{
	// TODO: Move into init_interface_kit().
	// Currently we can't do that, as get_modifier_key() blocks forever
	// when called on input_server initialization, since it tries
	// to send a synchronous message to itself (input_server is
	// a BApplication)

	if (get_modifier_key(B_LEFT_SHIFT_KEY, &value) != B_OK)
		value = 0x4b;
}


void
BMenu::_GetControlKey(uint32 &value) const
{
	// TODO: Move into init_interface_kit().
	// Currently we can't do that, as get_modifier_key() blocks forever
	// when called on input_server initialization, since it tries
	// to send a synchronous message to itself (input_server is
	// a BApplication)

	if (get_modifier_key(B_LEFT_CONTROL_KEY, &value) != B_OK)
		value = 0x5c;
}


void
BMenu::_GetCommandKey(uint32 &value) const
{
	// TODO: Move into init_interface_kit().
	// Currently we can't do that, as get_modifier_key() blocks forever
	// when called on input_server initialization, since it tries
	// to send a synchronous message to itself (input_server is
	// a BApplication)

	if (get_modifier_key(B_LEFT_COMMAND_KEY, &value) != B_OK)
		value = 0x66;
}


void
BMenu::_GetOptionKey(uint32 &value) const
{
	// TODO: Move into init_interface_kit().
	// Currently we can't do that, as get_modifier_key() blocks forever
	// when called on input_server initialization, since it tries
	// to send a synchronous message to itself (input_server is
	// a BApplication)

	if (get_modifier_key(B_LEFT_OPTION_KEY, &value) != B_OK)
		value = 0x5d;
}


void
BMenu::_GetMenuKey(uint32 &value) const
{
	// TODO: Move into init_interface_kit().
	// Currently we can't do that, as get_modifier_key() blocks forever
	// when called on input_server initialization, since it tries
	// to send a synchronous message to itself (input_server is
	// a BApplication)

	if (get_modifier_key(B_MENU_KEY, &value) != B_OK)
		value = 0x68;
}


void
BMenu::_CalcTriggers()
{
	BPrivate::TriggerList triggerList;

	// Gathers the existing triggers set by the user
	for (int32 i = 0; i < CountItems(); i++) {
		char trigger = ItemAt(i)->Trigger();
		if (trigger != 0)
			triggerList.AddTrigger(trigger);
	}

	// Set triggers for items which don't have one yet
	for (int32 i = 0; i < CountItems(); i++) {
		BMenuItem* item = ItemAt(i);
		if (item->Trigger() == 0) {
			uint32 trigger;
			int32 index;
			if (_ChooseTrigger(item->Label(), index, trigger, triggerList))
				item->SetAutomaticTrigger(index, trigger);
		}
	}
}


bool
BMenu::_ChooseTrigger(const char* title, int32& index, uint32& trigger,
	BPrivate::TriggerList& triggers)
{
	if (title == NULL)
		return false;

	index = 0;
	uint32 c;
	const char* nextCharacter, *character;

	// two runs: first we look out for alphanumeric ASCII characters
	nextCharacter = title;
	character = nextCharacter;
	while ((c = BUnicodeChar::FromUTF8(&nextCharacter)) != 0) {
		if (!(c < 128 && BUnicodeChar::IsAlNum(c)) || triggers.HasTrigger(c)) {
			character = nextCharacter;
			continue;
		}
		trigger = BUnicodeChar::ToLower(c);
		index = (int32)(character - title);
		return triggers.AddTrigger(c);
	}

	// then, if we still haven't found something, we accept anything
	nextCharacter = title;
	character = nextCharacter;
	while ((c = BUnicodeChar::FromUTF8(&nextCharacter)) != 0) {
		if (BUnicodeChar::IsSpace(c) || triggers.HasTrigger(c)) {
			character = nextCharacter;
			continue;
		}
		trigger = BUnicodeChar::ToLower(c);
		index = (int32)(character - title);
		return triggers.AddTrigger(c);
	}

	return false;
}


void
BMenu::_UpdateWindowViewSize(const bool &move)
{
	BMenuWindow* window = static_cast<BMenuWindow*>(Window());
	if (window == NULL)
		return;

	if (dynamic_cast<BMenuBar*>(this) != NULL)
		return;

	if (!fResizeToFit)
		return;

	bool scroll = false;
	const BPoint screenLocation = move ? ScreenLocation()
		: window->Frame().LeftTop();
	BRect frame = _CalcFrame(screenLocation, &scroll);
	ResizeTo(frame.Width(), frame.Height());

	if (fItems.CountItems() > 0) {
		if (!scroll) {
			if (fLayout == B_ITEMS_IN_COLUMN)
				window->DetachScrollers();

			window->ResizeTo(Bounds().Width(), Bounds().Height());
		} else {

			// Resize the window to fit the screen without overflowing the
			// frame, and attach scrollers to our cached BMenuWindow.
			BScreen screen(window);
			frame = frame & screen.Frame();
			window->ResizeTo(Bounds().Width(), frame.Height());

			// we currently only support scrolling for B_ITEMS_IN_COLUMN
			if (fLayout == B_ITEMS_IN_COLUMN) {
				window->AttachScrollers();

				BMenuItem* selectedItem = FindMarked();
				if (selectedItem != NULL) {
					// scroll to the selected item
					if (Supermenu() == NULL) {
						window->TryScrollTo(selectedItem->Frame().top);
					} else {
						BPoint point = selectedItem->Frame().LeftTop();
						BPoint superPoint = Superitem()->Frame().LeftTop();
						Supermenu()->ConvertToScreen(&superPoint);
						ConvertToScreen(&point);
						window->TryScrollTo(point.y - superPoint.y);
					}
				}
			}
		}
	} else {
		_CacheFontInfo();
		window->ResizeTo(StringWidth(BPrivate::kEmptyMenuLabel)
				+ fPad.left + fPad.right,
			fFontHeight + fPad.top + fPad.bottom);
	}

	if (move)
		window->MoveTo(frame.LeftTop());
}


bool
BMenu::_AddDynamicItems(bool keyDown)
{
	bool addAborted = false;
	if (AddDynamicItem(B_INITIAL_ADD)) {
		BMenuItem* superItem = Superitem();
		BMenu* superMenu = Supermenu();
		do {
			if (superMenu != NULL
				&& !superMenu->_OkToProceed(superItem, keyDown)) {
				AddDynamicItem(B_ABORT);
				addAborted = true;
				break;
			}
		} while (AddDynamicItem(B_PROCESSING));
	}

	return addAborted;
}


bool
BMenu::_OkToProceed(BMenuItem* item, bool keyDown)
{
	BPoint where;
	uint32 buttons;
	GetMouse(&where, &buttons, false);
	bool stickyMode = _IsStickyMode();
	// Quit if user clicks the mouse button in sticky mode
	// or releases the mouse button in nonsticky mode
	// or moves the pointer over another item
	// TODO: I added the check for BMenuBar to solve a problem with Deskbar.
	// BeOS seems to do something similar. This could also be a bug in
	// Deskbar, though.
	if ((buttons != 0 && stickyMode)
		|| ((dynamic_cast<BMenuBar*>(this) == NULL
			&& (buttons == 0 && !stickyMode))
		|| ((_HitTestItems(where) != item) && !keyDown))) {
		return false;
	}

	return true;
}


bool
BMenu::_CustomTrackingWantsToQuit()
{
	if (fExtraMenuData != NULL && fExtraMenuData->trackingHook != NULL
		&& fExtraMenuData->trackingState != NULL) {
		return fExtraMenuData->trackingHook(this,
			fExtraMenuData->trackingState);
	}

	return false;
}


void
BMenu::_QuitTracking(bool onlyThis)
{
	_SelectItem(NULL);
	if (BMenuBar* menuBar = dynamic_cast<BMenuBar*>(this))
		menuBar->_RestoreFocus();

	fState = MENU_STATE_CLOSED;

	if (!onlyThis) {
		// Close the whole menu hierarchy
		if (Supermenu() != NULL)
			Supermenu()->fState = MENU_STATE_CLOSED;

		if (_IsStickyMode())
			_SetStickyMode(false);

		if (LockLooper()) {
			be_app->ShowCursor();
			UnlockLooper();
		}
	}

	_Hide();
}


//	#pragma mark - menu_info functions


// TODO: Maybe the following two methods would fit better into
// InterfaceDefs.cpp
// In R5, they do all the work client side, we let the app_server handle the
// details.
status_t
set_menu_info(menu_info* info)
{
	if (!info)
		return B_BAD_VALUE;

	BPrivate::AppServerLink link;
	link.StartMessage(AS_SET_MENU_INFO);
	link.Attach<menu_info>(*info);

	status_t status = B_ERROR;
	if (link.FlushWithReply(status) == B_OK && status == B_OK)
		BMenu::sMenuInfo = *info;
		// Update also the local copy, in case anyone relies on it

	return status;
}


status_t
get_menu_info(menu_info* info)
{
	if (!info)
		return B_BAD_VALUE;

	BPrivate::AppServerLink link;
	link.StartMessage(AS_GET_MENU_INFO);

	status_t status = B_ERROR;
	if (link.FlushWithReply(status) == B_OK && status == B_OK)
		link.Read<menu_info>(info);

	return status;
}


extern "C" void
B_IF_GCC_2(InvalidateLayout__5BMenub,_ZN5BMenu16InvalidateLayoutEb)(
	BMenu* menu, bool descendants)
{
	menu->InvalidateLayout();
}
